﻿#region

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using log4net;

#endregion

namespace WikiBotDll
{
    /// <summary>Бот
    /// </summary>
    public class WikiBot
    {
        #region Consts

        /// <summary>Ключ на странице: не обрабатывать ботом
        /// ставится в любом месте не странице и означает, что бот не должен эту страницу обрабатывать
        /// т.е. пропустить
        /// </summary>
        public const string NoBotPageKey = "{{nobot}}";

        /// <summary>Ключ на странице: не обрабатывать ботом
        /// ставится в любом месте не странице и означает, что бот не должен эту страницу обрабатывать
        /// т.е. пропустить
        /// </summary>
        public const string NoBotUnderPageKey = "{{NoBotUnder}}";

        /// <summary>Что будет дописываться к названию статьи, чтобы получить название статьи-словаря
        /// </summary>
        public const string DictionaryAppendix = " - словарь";

        
        #endregion

        private static readonly ColorLogger log =
            new ColorLogger(LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType));

        private bool connected;

        /// <summary>Список всех страниц сайта
        /// </summary>
        private PageList pages;

        /// <summary>Адрес сервера Википедии
        /// </summary>
        public string ServerName { get; private set; }

        /// <summary>Имя пользователя для доступа к Википедии
        /// </summary>
        public string UserName { get; private set; }

        /// <summary>Пароль для доступа к Википедии
        /// </summary>
        public string Password { get; private set; }

        /// <summary>Сайт подключения бота к Википедии
        /// </summary>
        public Site Site { get; set; }

        /// <summary>Страница в вики
        /// </summary>
        public Page Page { get; set; }

        /// <summary>Подключиться к вики
        /// </summary>
        public bool Connected
        {
            get { return connected; }
        }

        /// <summary>Список страниц
        /// </summary>
        public PageList Pages
        {
            get { return pages; }
        }

        /// <summary>Словарь
        /// </summary>
        public WikiDictionary Dictionary { get; private set; }

        /// <summary>Подключить бота
        /// </summary>
        public void Connect(string serverName, string userName, string password)
        {
            #region validate input params
            if (string.IsNullOrEmpty(serverName)) throw new ArgumentNullException("serverName");
            if (string.IsNullOrEmpty(userName)) throw new ArgumentNullException("userName");
            if (string.IsNullOrEmpty(password)) throw new ArgumentNullException("password");
            #endregion

            ServerName = serverName;
            UserName = userName;
            Password = password;

            try
            {
                Site = new Site(
                    ServerName,
                    UserName,
                    Password,
                    ""
                    );

                Page = new Page(Site) {title = "User:" + UserName};
                Page.Load();
                Page.text = "Connected:" + DateTime.Now + "\n\n" + Page.text;
                Page.Save();
                pages = new PageList(Site);
                Dictionary = new WikiDictionary(Site);
                connected = true;
            }
            catch (Exception e)
            {
                connected = false;
                throw new Exception("Ошибка подключения к серверу", e);
            }
        }

        /// <summary>подгружаем в Pages имена всех страниц
        /// </summary>
        public void LoadListOfAllPages()
        {
            pages.FillFromAllPages("1", -1, true, 1000000);
        }

        /// <summary>заменяем в строке все слова, не входящие в скобки [[]] на нужное значение
        /// </summary>
        /// <param name="oldValues">Список слов, которые надо заменять</param>
        /// <param name="newValue">слово, на которое будет заменяться, напр [[Рыба в поэзии Бродского]]</param>
        /// <param name="sourceString">исходная строка</param>
        /// <returns>результирующая строка</returns>
        public static string ReplceWord(List<string> oldValues, string newValue, string sourceString)
        {
            #region validate input params

            if (oldValues == null || oldValues.Count == 0) throw new ArgumentNullException("oldValues");
            if (string.IsNullOrEmpty(sourceString)) throw new ArgumentNullException("sourceString");

            #endregion

            var noBotPos = sourceString.IndexOf(NoBotUnderPageKey);
            var noBotAppendix = "";
            if (noBotPos >= 0)
            {
                noBotAppendix = sourceString.Substring(noBotPos);
                sourceString = sourceString.Substring(0, noBotPos);
            }
            

            //если в строке вообще нет таких слов - возвращаем ту же строку
            CompareInfo ci = CultureInfo.CurrentCulture.CompareInfo;
            var oldValuesToReplace = new List<string>(); //список слов, которые действительно есть в строке (если есть)
            foreach (string oldValue in oldValues)
            {
                if (ci.IndexOf(sourceString, oldValue, CompareOptions.IgnoreCase) >= 0)
                    oldValuesToReplace.Add(oldValue);
            }
            if (oldValuesToReplace.Count == 0) //ни одного слова не нашли
                return sourceString + noBotAppendix;
            oldValuesToReplace.Sort();
            oldValuesToReplace.Reverse();
            var wordsInBrackets = new Regex(@"(\[\[.*?\]\]){1,}"); //регулярное выражение для поиска [[]]
            MatchCollection m = wordsInBrackets.Matches(sourceString);

            //Разбиваем строку на массив строк со скобками [[]] или без
            var strings = new List<string>();
            foreach (Match match in m)
            {
                if (match.Captures.Count > 0)
                {
                    int a = sourceString.IndexOf(match.Captures[0].Value);
                    if (a < 0)
                        break;
                    if (a > 0)
                        strings.Add(sourceString.Substring(0, a));
                    strings.Add(match.Captures[0].Value);
                    sourceString = sourceString.Remove(0, match.Captures[0].Value.Length + a);
                }
            }

            if (sourceString != "") //если остался хвостик, добавляем
                strings.Add(sourceString);
            sourceString = "";
            //проходим по всем строкам без скобок и заменяем слово на нужное
            bool modified = false;
            foreach (string s in strings)
            {
                if (!modified)
                    if (!s.Contains("[") && !s.Contains("]"))
                    {
                        string tempstr = s;
                        foreach (string oldValue in oldValuesToReplace)
                        {
                            MatchCollection mm = new Regex(@"(?i)(\b" + oldValue + @"\b){1,}").Matches(tempstr);

                            if (mm.Count > 0)
                            {
                                string a = mm[0].Captures[0].Value;
                                a = "[[" + newValue + "|" + a + "]]";

                                tempstr = tempstr.Remove(mm[0].Captures[0].Index, mm[0].Captures[0].Length);
                                tempstr = tempstr.Insert(mm[0].Captures[0].Index, a);
                                modified = true;
                                break;
                            }
                        }
                        sourceString += tempstr;
                    }
                    else
                        sourceString += s;
                else
                    sourceString += s;
            }

            //Если были произведены изменения, то необходимо заново пройтись и убедиться, что больше нет слов без скобок
            //а если есть, сделать для них линк и опять пройтись, до тех пор, пока не будут пролинкованы все слова (т.е. заменены на ссылку со скобками)
            if (modified)
                sourceString = ReplceWord(oldValues, newValue, sourceString);
            return sourceString + noBotAppendix;
        }

        #region Public-static methods

        /// <summary>Обработать страницу (заменить слова по словарю на ссылку). Выполняется воркером
        /// </summary>
        /// <param name="page">Обрабатываемая страница</param>
        /// <param name="words">Словарь</param>
        /// <returns>Обработана ли страница</returns>
        public static bool ProcessPage(Page page, WikiWords words)
        {
            #region validate input params

            if (page == null) throw new ArgumentNullException("page");

            #endregion

            page.Load();
            if (page.text.Contains(NoBotPageKey))
            {
                return false;
            }

            string newPageText = ReplceWord(words.Values, words.LinkPageName, page.text);

            if (page.text != newPageText)
            {
                page.text = newPageText;
                page.Save("BOT: replace " + words.Values[0] + " etc. to " + words.LinkPageName, true);
                return true;
            }

            return false;
        }

        /// <summary>Обработать страницу (заменить слова по словарю на ссылку). Выполняется воркером
        /// </summary>
        /// <param name="page">Обрабатываемая страница</param>
        /// <param name="dictionary">словарь</param>
        /// <returns>Обработана ли страница</returns>
        public static bool ProcessPage(Page page, List<WikiWords> dictionary)
        {
            #region validate input params

            if (page == null) throw new ArgumentNullException("page");

            #endregion

            page.Load();
            if (page.text.Contains(NoBotPageKey))
            {
                return false;
            }

            string newPageText = page.text;

            foreach (var words in dictionary)
            {
                newPageText = ReplceWord(words.Values, words.LinkPageName, newPageText);
            }

            if (page.text != newPageText)
            {
                page.text = newPageText;
                page.Save("BOT: replace by dictuinary", true);
                return true;
            }

            return false;
        }

        #endregion
    }


    /// <summary>Словарь и ссылка на статью
    /// </summary>
    public class WikiWords
    {
        private static readonly ColorLogger log =
            new ColorLogger(LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType));

        #region consts

        /// <summary>Название корня xml-файла со словами
        /// </summary>
        public const string XmlWordsRoot = "Words";

        /// <summary>Название элемента слова xml-файла со словами
        /// </summary>
        public const string XmlWordsWordName = "Word";

        /// <summary>Страница, на которую должны ссылаться слова
        /// </summary>
        public const string XmlWordLinkPage = "LinkPage";

        #endregion

        private string linkPageName;

        /// <summary>Список слов
        /// </summary>
        private List<string> values = new List<string>();

        public WikiWords(Site site)
        {
            Site = site;
        }

        /// <summary>Подключение к вики
        /// </summary>
        public Site Site { get; private set; }

        /// <summary>Ссылка на страницу
        /// </summary>
        public string LinkPageName
        {
            get { return linkPageName; }
            set { linkPageName = value.Replace(WikiBot.DictionaryAppendix, ""); }
        }

        /// <summary>Страница словаря вида "название статьи - словарь"
        /// </summary>
        public string LinkDictionaryPageName
        {
            get { return LinkPageName + WikiBot.DictionaryAppendix; }
            set { linkPageName = value.Replace(WikiBot.DictionaryAppendix, ""); }
        }

        /// <summary>Список слов
        /// </summary>
        public List<string> Values
        {
            get { return values; }
            set { values = value; }
        }

        /// <summary>Сохранить слова в словарь на сайте
        /// </summary>
        public void SaveToPage()
        {
            #region validate input params

            if (Site == null) throw new ArgumentNullException("Site");

            #endregion

            var page = new Page(Site)
                           {
                               title = LinkDictionaryPageName
                           };

            foreach (var word in Values)
            {
                page.text += word + "\n\n";
            }

            page.text += "\n\n" + WikiBot.NoBotPageKey;
            page.Save("BOT: save dictionary", false);
        }

        /// <summary>Загрузить слова со страницы словаря
        /// </summary>
        /// <returns></returns>
        public void LoadWordsFromDicPage()
        {
            #region validate input params

            if (Site == null) throw new ArgumentNullException("Site");

            #endregion

            var page = new Page(Site) {title = LinkDictionaryPageName};

            page.Load();

            var strs = page.text.Split('\n');

            foreach (var str in strs)
            {
                var st = str.Trim('\n');
                if (!string.IsNullOrEmpty(st) && !st.Equals(WikiBot.NoBotPageKey))
                {
                    Values.Add(st);
                }
            }
        }

        /// <summary>Загрузка в список списка слов из xml
        /// </summary>
        /// <param name="filename">папка файла</param>
        /// <returns></returns>
        public void LoadWordsFromXml(string filename)
        {
            #region validate input params

            if (string.IsNullOrEmpty(filename)) throw new ArgumentNullException("filename");

            #endregion

            var doc = XDocument.Load(filename);
            values.Clear();

            if (doc.Element(XmlWordsRoot) != null)
            {
                try
                {
                    foreach (XElement word in doc.Element(XmlWordsRoot).Elements(XmlWordsWordName))
                    {
                        Values.Add(word.Value);
                    }
                }
                catch (Exception ex)
                {
                    log.Error("Error LoadWordsFromXml XmlWordsRoot for forlder " + filename + ": " + ex.Message, ex);
                }
            }

            try
            {
                LinkPageName = doc.Element(XmlWordsRoot).Element(XmlWordLinkPage).Value;
            }
            catch (Exception ex)
            {
                log.Error("Error LoadWordsFromXml  LinkPageName for forlder " + filename + ": " + ex.Message, ex);
                LinkPageName = "";
            }
        }

        /// <summary>Сохранить слова в XML
        /// </summary>
        /// <param name="folder">папка файла</param>
        public void SaveWordsToXml(string folder)
        {
            #region validate input params

            if (string.IsNullOrEmpty(folder)) throw new ArgumentNullException("folder");

            #endregion

            try
            {
                var doc = new XDocument(new XElement(XmlWordsRoot));
                var fileName = folder + linkPageName + ".xml";

                foreach (string word in Values)
                {
                    doc.Element(XmlWordsRoot).Add(new XElement(XmlWordsWordName, word));
                }

                doc.Element(XmlWordsRoot).Add(new XElement(XmlWordLinkPage, LinkPageName));

                doc.Save(fileName);
            }
            catch (Exception ex)
            {
                log.Error("Error LoadWordsFromXml  LinkPageName for forlder " + folder + ": " + ex.Message, ex);
            }
        }
    }


    /// <summary>Словарное слово с ссылкой на словарь
    /// </summary>
    public class LinkedWord
    {
        private static readonly ColorLogger log =
            new ColorLogger(LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType));

        private string linkPageName;

        /// <summary>Слово
        /// </summary>
        public string Value { get; set; }

        /// <summary>Ссылка на страницу
        /// </summary>
        public string LinkPageName
        {
            get { return linkPageName; }
            set { linkPageName = value.Replace(WikiBot.DictionaryAppendix, ""); }
        }

        /// <summary>Страница словаря вида "название статьи - словарь"
        /// </summary>
        public string LinkDictionaryPageName
        {
            get { return LinkPageName + WikiBot.DictionaryAppendix; }
            set { linkPageName = value.Replace(WikiBot.DictionaryAppendix, ""); }
        }
    }

    /// <summary>Словарь
    /// </summary>
    public class WikiDictionary
    {
        private static readonly ColorLogger log =
            new ColorLogger(LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType));

        private readonly List<WikiWords> words = new List<WikiWords>();

        /// <summary>Конструктор
        /// </summary>
        /// <param name="site">подключение к сайту вики</param>
        public WikiDictionary(Site site)
        {
            Site = site;
        }

        /// <summary>Подключение к сайту вики
        /// </summary>
        public Site Site { get; private set; }

        public bool Modified { get; set; }

        /// <summary>Слова словаря
        /// </summary>
        public List<WikiWords> Words
        {
            get { return words; }
        }

        /// <summary>Добавить слово с ссылкой на словарь в словарь
        /// </summary>
        /// <param name="linkedWord">слово</param>
        public void AddWordToDictionary(LinkedWord linkedWord)
        {
            #region validate input params

            if (linkedWord == null) throw new ArgumentNullException("linkedWord");
            if (string.IsNullOrEmpty(linkedWord.Value)) throw new ArgumentNullException("linkedWord");
            if (string.IsNullOrEmpty(linkedWord.LinkPageName)) throw new ArgumentNullException("linkedWord");

            #endregion

            if (linkedWord.LinkPageName.Contains("Участник:") ||
                linkedWord.LinkPageName.Contains("User:") ||
                linkedWord.LinkPageName.Contains("Медиа:"))
            {
                return;
            }

            var linkPageExists = false;

            for (int i = 0; i < words.Count; i++)
            {
                if (words[i].LinkPageName.Equals(linkedWord.LinkPageName,
                                                 StringComparison.CurrentCultureIgnoreCase))
                {
                    linkPageExists = true;
                    var alreadyExists = false;
                    foreach (var value in words[i].Values)
                    {
                        if (value.Equals(linkedWord.Value, StringComparison.CurrentCultureIgnoreCase))
                        {
                            alreadyExists = true;
                            break;
                        }
                    }

                    if (!alreadyExists)
                    {
                        words[i].Values.Add(linkedWord.Value);
                        words[i].SaveToPage();
                    }
                    break;
                }
            }

            if (!linkPageExists)
            {
                var tmpWords = new WikiWords(Site) {LinkPageName = linkedWord.LinkPageName};
                tmpWords.Values.Add(linkedWord.Value);
                Words.Add(tmpWords);
                tmpWords.SaveToPage();
            }
        }

        /// <summary>Загрузить словарь с сайта
        /// </summary>
        /// <param name="pages">список страниц</param>
        public void Load(PageList pages)
        {
            #region validate input params

            if (pages.Count() <= 0)
            {
                throw new Exception("pages count <= 0");
            }

            if (Site == null) throw new ArgumentNullException("Site");

            #endregion

            foreach (Page page in pages)
            {
                if (page.title.Contains(WikiBot.DictionaryAppendix))
                {
                    var tmpWords = new WikiWords(Site) {LinkPageName = page.title};
                    tmpWords.LoadWordsFromDicPage();
                    Words.Add(tmpWords);
                }
            }
            Modified = false;
        }

        /// <summary>Дополняем словарь
        /// Заполнить словари по содержимому страниц
        /// проходим по всем страницам и находим новые слова и ссылки
        /// </summary>
        public void ProcessAllPagesToDictionay(PageList pages)
        {
            #region validate input params

            if (pages == null || pages.Count() <= 0) throw new ArgumentNullException("pages");

            #endregion

            try
            {
                foreach (Page page in pages)
                {
                    page.Load();
                    if (!page.text.Contains(WikiBot.NoBotPageKey))
                    {
                        ProcessPageToDictionay(page.text);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Error ProcessAllPagesToDictionay: " + ex.Message, ex);
            }
        }

        /// <summary>Викифицируем страницы
        /// проходим по всем страницам и находим
        /// слова, которые должны иметь ссылки на статьи
        /// добавляем ссылки на стать в страницу
        /// </summary>
        public void ProcessAllPagesFromDictionary(PageList pages)
        {
            try
            {
                foreach (Page page in pages)
                {
                    if (!page.title.Contains(WikiBot.DictionaryAppendix))
                        WikiBot.ProcessPage(page, Words);
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Error ProcessAllPagesToDictionay: " + ex.Message);
            }
        }

        /// <summary>Анализируем страницу
        /// </summary>
        /// <param name="text">текст страницы</param>
        private void ProcessPageToDictionay(string text)
        {
            #region validate input params

            if (string.IsNullOrEmpty(text)) throw new ArgumentNullException("text");

            #endregion

            var analizeString = text;

            var i = text.IndexOf(WikiBot.NoBotUnderPageKey);
            if (i >= 0)
            {
                if (i > 2)
                {
                    analizeString = analizeString.Substring(0, i - 2);
                }
                else
                {
                    return;
                }
            }
            var wordsInBrackets = new Regex(@"(\[\[.*?\]\]){1,}"); //регулярное выражение для поиска [[]]
            var m = wordsInBrackets.Matches(analizeString);

            //Ищем ссылки вида [[имя статьи|слово]] и составляем список (исключая повторения слов)
            foreach (Match match in m)
            {
                if (match.Captures.Count > 0)
                {
                    var st = match.Captures[0].Value.Trim('[', ']').Split('|');
                    if (st.Length != 2)
                        break;
                    var word = new LinkedWord {LinkPageName = st[0], Value = st[1]};
                    AddWordToDictionary(word);
                }
            }

        }
    }
}